﻿/*
* SHARPUPNP
* 
* Written by Marco Minerva, mailto:marco.minerva@gmail.com
* 
* This code is released under the GNU General Public License (GPL) v2.
* A copy of this license is available at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
*/

using System;
using System.Text;
using System.Diagnostics;
using System.Net.Sockets;
using System.Net;
using System.Collections.Generic;
using System.Threading;
using System.Xml;
//using System.Xml.Linq;
//using System.Linq;

namespace SharpUpnp
{
    /// <summary>
    /// Provides a set of static methods that allow to interact with the UPnP compliant devices that are available in the local network.
    /// </summary>
    public static class Upnp
    {
        private const string BROADCAST_ADDRESS = "239.255.255.250";
        private const int BROADCAST_ADDRESS_PORT = 1900;
        private const string BROADCAST_MESSAGE = "M-SEARCH * HTTP/1.1\r\n" +
                                                "HOST: {0}:{1}\r\n" +
                                                "ST: upnp:rootdevice\r\n" +
                                                "MAN: \"ssdp:discover\"\r\n" +
                                                "MX: 3\r\n" +
                                                "\r\n" +
                                                "\r\n";

        internal const string WAN_IP_CONNECTION = "urn:schemas-upnp-org:service:WANIPConnection:1";
        internal const string WAN_PPP_CONNECTION = "urn:schemas-upnp-org:service:WANPPPConnection:1";
        internal const string WAN_DEVICE_TYPE = "urn:schemas-upnp-org:device:WANConnectionDevice:1";
        internal const string DEFAULT_NAMESPACE = "urn:schemas-upnp-org:device-1-0";

        /// <summary>
        /// Returns the NAT devices that are available in the local network. The UPnP devices are discovered sending the appropriate UDP message on the port number 1900. 
        /// </summary>
        /// <returns>An array containing the list of the UPnP NAT devices that were found in the local network.</returns>
        /// <remarks>The entire discovery procedure is described at http://zbowling.com/projects/upnp/. Since the protocol used is UDP, the message is sent two times in order to improve the reliability of the procedure.</remarks>
        public static UpnpNatDevice[] GetNatDevices()
        {
            return GetNatDevices(2);
        }

        /// <summary>
        /// Returns the NAT devices that are available in the local network. The UPnP devices are discovered sending the appropriate UDP message on the port number 1900. 
        /// </summary>
        /// <param name="attempts">The number of times to send the discovery message.</param>
        /// <returns>An array containing the list of the UPnP NAT devices that were found in the local network.</returns>
        /// <exception cref="System.ArgumentException"><em>attempts</em> is equal or less than 0.</exception>
        /// <remarks>The entire discovery procedure is described at http://zbowling.com/projects/upnp/. Since the protocol used is UDP, in order to improve the reliability of the procedure it is raccommended to send the discovery message more than one time; if not specified otherwise, the message is sent two times.</remarks>
        public static UpnpNatDevice[] GetNatDevices(int attempts)
        {
            //Controlla se gli argomenti specificati sono corretti.
            if (attempts <= 0)
                throw new ArgumentException("The attempts number must be grater than 0");

            string query = string.Format(BROADCAST_MESSAGE, BROADCAST_ADDRESS, BROADCAST_ADDRESS_PORT);

            //use sockets instead of UdpClient so we can set a timeout easier.
            Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            client.SendTimeout = 3000;
            client.ReceiveTimeout = 3000;

            IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(BROADCAST_ADDRESS), BROADCAST_ADDRESS_PORT);
            EndPoint sender = new IPEndPoint(IPAddress.Any, 0);

            HashSet<string> responses = new HashSet<string>();
            byte[] buffer = Encoding.ASCII.GetBytes(query);
            for (int i = 0; i < attempts; i++)
            {
                try
                {
                    client.SendTo(buffer, buffer.Length, SocketFlags.None, endPoint);

                    byte[] response = new byte[1024];
                    int recv = client.ReceiveFrom(response, ref sender);
                    if (recv > 0)
                        responses.Add(Encoding.ASCII.GetString(response, 0, recv));

                    //Cicla finché sono disponibili ulteriori risposte.
                    while (client.Available > 0)
                    {
                        Array.Clear(response, 0, response.Length);
                        recv = client.ReceiveFrom(response, ref sender);
                        if (recv > 0)
                            responses.Add(Encoding.ASCII.GetString(response, 0, recv));
                    }
                }
                catch (Exception ex)
                {
                    Debug.WriteLine(ex.Message);
                }
            }

            //Crea la lista che conterrà l'elenco delle periferiche NAT trovate.
            List<UpnpNatDevice> devices = new List<UpnpNatDevice>();

            if (responses.Count == 0)
                return devices.ToArray();

            /* QueryResult is something like this:
            *
            HTTP/1.1 200 OK
            Cache-Control:max-age=60
            Location:http://10.10.10.1:80/upnp/service/des_ppp.xml
            Server:NT/5.0 UPnP/1.0
            ST:upnp:rootdevice
            EXT:

            USN:uuid:upnp-InternetGatewayDevice-1_0-00095bd945a2::upnp:rootdevice
            */

            //Using the location url, we get more information:
            using (WebClient webClient = new WebClient())
            {
                foreach (string queryResponse in responses)
                {
                    string location = string.Empty;
                    string[] parts = queryResponse.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
                    foreach (string part in parts)
                    {
                        if (part.ToLower().StartsWith("location"))
                        {
                            //Estrae l'URI della periferica.
                            location = part.Substring(part.IndexOf(':') + 1);
                            break;
                        }
                    }
                    if (location.Length == 0)
                        continue;

                    try
                    {
                        string ret = webClient.DownloadString(location);
                        if (ret.Contains(WAN_IP_CONNECTION) || ret.Contains(WAN_PPP_CONNECTION))
                        {
                            Uri uri = new Uri(location);
                            UpnpNatDevice device = new UpnpNatDevice(new IPEndPoint(IPAddress.Parse(uri.Host), uri.Port));
                            if (ret.Contains(WAN_IP_CONNECTION))
                                device.ServiceType = WAN_IP_CONNECTION;
                            else
                                device.ServiceType = WAN_PPP_CONNECTION;

                            ////Utilizza XLinq per estrarre le informazioni dal file XML.
                            //XDocument doc = XDocument.Parse(ret);
                            ////Estre tutte le periferiche NAT.
                            //var query1 = from node in doc.Descendants(XName.Get("device", DEFAULT_NAMESPACE))
                            //        where node.Element(XName.Get("deviceType", DEFAULT_NAMESPACE)).Value == WAN_DEVICE_TYPE
                            //        select node;
                            ////Per ogni periferica trovata, estrae il suo nome e il suo ControlURL.
                            //foreach (var natDevice in query1)
                            //{
                            //    var query2 = from node in natDevice.Descendants(XName.Get("service", DEFAULT_NAMESPACE))
                            //             where node.Element(XName.Get("serviceType", DEFAULT_NAMESPACE)).Value == device.ServiceType
                            //             select new { DeviceName = natDevice.Element(XName.Get("friendlyName", DEFAULT_NAMESPACE)).Value, ControlURL = node.Element(XName.Get("controlURL", DEFAULT_NAMESPACE)).Value };
                            //    //Infine, aggiunge ognuna delle periferiche individuate all'elenco.
                            //    foreach (var deviceInfo in query2)
                            //    {
                            //        device.Name = deviceInfo.DeviceName;
                            //        device.ControlUrl = deviceInfo.ControlURL;
                            //        devices.Add(device);
                            //    }
                            //}                          

                            //Carica la risposta in un file XmlDocument per estrarne le informazioni.
                            XmlDocument doc = new XmlDocument();
                            doc.LoadXml(ret);
                            //Create an XmlNamespaceManager for resolving namespaces.
                            XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
                            nsmgr.AddNamespace("u", DEFAULT_NAMESPACE);
                            //Estrae il nome (friendly name) della periferica.
                            string xPath = string.Format("descendant::u:device[u:deviceType='{0}']", WAN_DEVICE_TYPE);
                            device.Name = doc.SelectSingleNode(xPath, nsmgr).ParentNode.SelectSingleNode("descendant::u:friendlyName", nsmgr).InnerText;
                            //Estrae il Control URL del servizio.
                            xPath = string.Format("descendant::u:service[u:serviceType='{0}']", device.ServiceType);
                            device.ControlUrl = doc.SelectSingleNode(xPath, nsmgr).SelectSingleNode("descendant::u:controlURL", nsmgr).InnerText;

                            devices.Add(device);
                        }
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.Message);
                    }
                }
            }

            return devices.ToArray();
        }
    }
}
